-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enumoji implementation in C# 14.0 #2
base: master
Are you sure you want to change the base?
Conversation
jbtule
commented
Sep 10, 2020
•
edited
Loading
edited
- Lighter weight syntax reduces LOC by half with identical calls. (direct port)
- Enumojis
- Switched to blog emojis (compound 🧑🚀 emoji in master wasn't overwriting properly on Mac OS X terminal)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example wasn't ported functionally, just syntactically from C#. It about half the lines of code of the original C#. I've annotated this PR to clarify the differences in F#. For further information how how F# is actually a great general purpose programing language, and that it expresses functional, imperative and object oriented code more tersely that C# check out this slideshow: C# Light
let timeout = 500 | ||
let mutable runSimulation = true | ||
|
||
type Status = ``💀`` = 0 | ``😁`` = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Enumoji's we don't have to use conditional expressions for the 3 data types needed for two states Alive|Dead in this port. We can then use the F# operators string
, int
, and enum
to convert to what we need.
let rows = 15 | ||
let columns = 15 | ||
let timeout = 500 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can infer types on our statically scoped variables!
let rows = 15 | ||
let columns = 15 | ||
let timeout = 500 | ||
let mutable runSimulation = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F# requires you to qualify any variable you want to mutate. You save so much boiler plate with type inference, adding this extra 6 let word hardly seems like a thing, and makes it easier to reason about variables. Notice we only have to use the keyword 3 times in this program ported directly from C#.
|
||
type Status = ``💀`` = 0 | ``😁`` = 1 | ||
|
||
let private nextGeneration (currentGrid: Status [,]) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although there is a recursive modifier (rec
) that can be applied to a module or namespace, typically you declare your functions before you use them in F#. So nextGeneration
helper function was moved to the top of the file in this port.
@@ -0,0 +1,67 @@ | |||
module GameOfLife.Program |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although you can do C# style static classes in F# with some boilerplate. A module is the preferred vehicle for static functions. CLI wise it will look like a static class in C#.
| Status.``💀`` when aliveNeighbors = 3 -> Status.``😁`` | ||
// stays the same | ||
| unchanged -> unchanged | ||
nextGeneration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In F# the last line in an expression is the returned value. No return
keyword necessary, but that also means no easy short circuiting.
let sb = StringBuilder() | ||
for row in 0..(rows-1) do | ||
for column in 0..(columns-1) do | ||
future.[row, column] |> string |> sb.Append |> ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pipe operator |>
makes it easy to describe data flowing from one function to another. Otherwise it would have been written with nested parentheses:
ignore(sb.Append(string(future.[row, column])))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The string
operator calls ToString()
on an object. And the ignore
operator is how you discard return values that you are not going to use (Like Append returns a reference to the string builder it's mutating. You'll get a warning if you have an expression that returns a value that isn't unit and it's not the last line of it's enclosing expression.
[<EntryPoint>] | ||
let main _ = | ||
// randomly initialize our grid | ||
let mutable grid = Array2D.init rows columns (fun _ _ -> RandomNumberGenerator.GetInt32(0, 2) |> enum) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F# array creation functions let you pass a lambda function to be used in initialization. Lambda's are prefixed with fun
in F#, it's fun! Since we are randomly filling out the array, I discard the row and column arguments in the lambda. We can use the enum
operator which is actually able to infer the enum type statically in this case!!
Console.CancelKeyPress.Add( | ||
fun _ -> | ||
runSimulation <- false | ||
Console.WriteLine("\n👋 Ending simulation.")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I read somewhere that C# designers wished they never special cased +=
and -=
operators to be AddHandler
and RemoveHandler
methods. So those methods are what you can use in F# instead of the operators. To make things simpler without explicitly creating delegates, you can use the Add
method with a lambda, as long as you don't need to remove the handler later.
while runSimulation do | ||
print grid | ||
grid <- nextGeneration(grid) | ||
0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Main function is expected to return an integer. So last line is a zero.